Rust 中的拷贝和克隆
克隆(Clone)
克隆是一种深拷贝(deep copy)机制,可以用于任何类型,包括堆上分配的数据:
- 定义特点:通过实现 Clone 特征并调用 clone() 方法,可能会复制堆上的数据。
- 适用类型:
- 所有类型都可以实现
Clone
特征 - 例如,
String
、Vec<T>
、Box<T>
等
- 所有类型都可以实现
- 行为特点:
- Rust 永远也不会自动创建数据的 深拷贝。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。
- 如果需要深度复制数据,可以实现
Clone
特征,同时必须实现clone
方法 - 会复制所有数据,包括堆上分配的数据,性能开销可能较大,特别是对于大型数据结构
let s1 = String::from("hello");
let s2 = s1.clone(); // 显式克隆,s1和s2都有效
println!("s1 = {}, s2 = {}", s1, s2); // 正常工作
这段代码能够正常运行,说明 s2
确实完整的复制了 s1
的数据。
如果代码性能无关紧要,例如初始化程序时或者在某段时间只会执行寥寥数次时,你可以使用 clone
来简化编程。但是对于执行较为频繁的代码(热点路径),使用 clone
会极大的降低程序性能,需要小心使用!
拷贝(Copy)
拷贝是一种浅拷贝(shallow copy)机制,只适用于存储在栈上的简单数据类型:
- 特点:通过实现 Copy 特征来启用,复制只发生在栈上,因此性能很高,在日常编程中,浅拷贝无处不在。
- 适用类型:
- 所有整数类型,如 u32、i32
- 布尔类型 bool
- 浮点数类型,如 f64
- 字符类型 char
- 元组,当且仅当其包含的类型也都实现了
Copy
特征,比如,(i32, i32)
是Copy
的,但(i32, String)
就不是 - 不可变引用&T(但可变引用 &mut T 不可 Copy)
- 行为特点:
- 自动发生,不需要显式调用任何方法(例如 clone)
- 实现
Copy
必须先实现Clone
- 复制时不会发生所有权转移,复制后,原值和新值都可以使用,修改一个不会影响另一个
fn main() {
let x = 5;
let y = x; // x被复制给y,x仍然可用,这里没有发生所有权的转移
println!("x: {}, y: {}", x, y); // 输出:x: 5, y: 5
}
这段代码没有调用 clone
,不过依然实现了类似深拷贝的效果 —— 没有报所有权的错误。原因是像整型这样的基本类型在编译时是已知大小的,会被存储在栈上,所以拷贝其实际的值是快速的。这意味着没有理由在创建变量 y
后使 x
无效(x
、y
都仍然有效)。换句话说,这里没有深浅拷贝的区别,因此这里调用 clone
并不会与通常的浅拷贝有什么不同,我们可以不用管它(可以理解成在栈上做了深拷贝)。
复制值的特征 Copy 和 Clone
在 Rust 中,Copy
和 Clone
是两个用于对象复制的 trait。它们之间有一些重要的区别。 Clone
特征用于创建一个值的深拷贝(deep copy),复制过程可能包含代码的执行以及堆上数据的复制。
派生 Clone
实现了 clone
方法,当为整个的类型实现 Clone
时,在该类型的每一部分上都会调用 clone
方法。这意味着类型中所有字段或值也必须实现了 Clone
,这样才能够派生 Clone
。
例如,当在一个切片(slice)上调用 to_vec
方法时, Clone
是必须的。切片只是一个引用,并不拥有其所包含的实例数据,但是从 to_vec
中返回的 Vector 需要拥有实例数据,因此, to_vec
需要在每个元素上调用 clone
来逐个复制。因此,存储在切片中的类型必须实现 Clone
。
一个类型要实现 Copy
必须先实现 Clone
,因为 Copy
是 Clone
的子特征。当性能是关键因素时,应该优先考虑 Copy
而不是 Clone
,因为深拷贝会导致性能下降。 Copy
特征允许你通过只拷贝存储在栈上的数据来复制值(浅拷贝),
当一个类型的内部字段全部实现了 Copy
时,你就可以在该类型上派上 Copy
特征。 一个类型如果要实现 Copy
它必须先实现 Clone
特征,因为一个类型实现 Clone
后,就等于顺便实现了 Copy
。
#[derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
总之, Copy
拥有更好的性能,当浅拷贝足够的时候,就不要使用 Clone
,不然会导致你的代码运行更慢,对于性能优化来说,一个很大的方面就是减少热点路径深拷贝的发生。
其他
所有权转移其实可以分为两类:栈上数据的复制和堆上数据的转移,这也是非常符合直觉的,例如i32
这种类型实现了Copy
特征,可以存储在栈上,因此它就是复制行为,而String
类型是引用存储在栈上,底层数据存储在堆上,因此转移所有权时只需要复制一下引用即可。(Rust中基本类型的赋值操作表现出 copy 的行为,复合类型的赋值操作表现出 move 的行为,如果想让复合类型的赋值操作表现出 copy 的行为,就要显式的调用 clone)
- 对于栈上数据(如数组),move 操作可能导致深拷贝,导致性能下降,例如下面的LargeArray:
struct LargeArray {
a: [i128; 10000],
}
结构体是一个复合类型,它内部字段的数据存在哪里,就大致决定了它存在哪里。而该结构体里面的a
字段是一个数组,而不是动态数组 Vec
,数组是存储在栈上的数据结构。所以 LargeArray 是存于栈上的数据结构。这种类型发生移动时,会复制所有数据,此时性能较低,可以用 Box 让其存储在堆上,从而避免复制所有数据。
- 对于堆上数据(如Box<T>或String),move 操作只复制引用
fn main() {
// 在栈上创建一个长度为1000的数组
let arr = [0;1000];
// 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是自动调用 Copy 直接重新深拷贝了一份数据
let arr1 = arr;
// arr 和 arr1 都拥有各自的栈上数组,因此不会报错
println!("{:?}", arr.len());
println!("{:?}", arr1.len());
// 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它
let arr = Box::new([0;1000]);
// 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝
// 所有权顺利转移给 arr1,arr 不再拥有所有权
let arr1 = arr;
println!("{:?}", arr1.len());
// 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
// println!("{:?}", arr.len());
}